﻿/*
 * LINQ to LDAP
 * http://linqtoldap.codeplex.com/
 * 
 * Copyright Alan Hatter (C) 2010-2012
 * alan.hatter@gmail.com - http://hattercoding.blogspot.com/
 * 
 * This project is subject to licensing restrictions. Visit http://linqtoldap.codeplex.com/license for more information.
 */

using System;
using System.Collections.Generic;
using System.DirectoryServices.Protocols;
using System.IO;
using System.Linq;
using LinqToLdap.Collections;
using LinqToLdap.Exceptions;
using LinqToLdap.Helpers;
using LinqToLdap.Logging;
using LinqToLdap.Mapping;
using LinqToLdap.Transformers;

namespace LinqToLdap
{
    /// <summary>
    /// Implementation for performing LINQ queries against a directory
    /// </summary>
    public class DirectoryContext : IDirectoryContext
    {
        private bool _disposed;
        private readonly bool _disposeOfConnection = true;
        private LdapConnection _connection;
        private ILdapConfiguration _configuration;

        private readonly List<DirectoryQueryProvider> _providers = new List<DirectoryQueryProvider>();

        /// <summary>
        /// Creates an instance and uses the specified <paramref name="connection"/> for querying.  
        /// The connection will not be disposed of when this instance is disposed.
        /// </summary>
        /// <param name="connection">Connection to use</param>
        /// <param name="disposeOfConnection">Indicates if the context should dispose of the connection when <see cref="Dispose"/> is called.</param>
        /// <param name="configuration">The configuration.</param>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="connection"/> is null</exception>
        public DirectoryContext(LdapConnection connection, bool disposeOfConnection = false, ILdapConfiguration configuration = null)
        {
            if (connection == null) throw new ArgumentNullException("connection");

            if (configuration == null)
            {
                configuration = LdapConfiguration.Configuration ?? new LdapConfiguration();
            }
            _configuration = configuration;
            _disposeOfConnection = disposeOfConnection;
            _connection = connection;
        }

        /// <summary>
        /// Creates an instance for querying.
        /// The underlying connection will be disposed of when this instance is disposed if the <see cref="LdapConfiguration"/> doeas not
        /// have a configured <see cref="ILdapConnectionFactory"/>.  Otherwise <see cref="ILdapConnectionFactory.ReleaseConnection"/> will be called.
        /// </summary>
        /// <exception cref="MappingException">
        /// Thrown if <paramref name="configuration"/> or <see cref="ILdapConfiguration.ConnectionFactory"/> is null.
        /// </exception>
        /// <param name="configuration">The configuration.</param>
        public DirectoryContext(ILdapConfiguration configuration)
        {
            if (configuration == null ||  configuration.ConnectionFactory == null)
                throw new MappingException("configuration cannot be null and a connection factory must be provided.");

            _configuration = configuration;
            _disposeOfConnection = true;
            Logger = _configuration.Log;
            _connection = _configuration.ConnectionFactory.GetConnection();
        }

        /// <summary>
        /// Creates an instance for querying.
        /// The underlying connection will be disposed of when this instance is disposed if the <see cref="LdapConfiguration"/> doeas not
        /// have a configured <see cref="ILdapConnectionFactory"/>.  Otherwise <see cref="ILdapConnectionFactory.ReleaseConnection"/> will be called.
        /// </summary>
        /// <exception cref="MappingException">Thrown if <see cref="LdapConfiguration"/> has not been initialized.</exception>
        public DirectoryContext()
        {
            if (LdapConfiguration.Configuration == null || LdapConfiguration.Configuration.ConnectionFactory == null)
                throw new MappingException("A static configuration and connection factory must be provided.  See LdapConfiguration.");

            _disposeOfConnection = true;
            _configuration = LdapConfiguration.Configuration;
            Logger = _configuration.Log;
            _connection = _configuration.ConnectionFactory.GetConnection();
        }

        /// <summary>
        /// Allows for logging filters and errors.
        /// </summary>
        public ILinqToLdapLogger Logger { private get; set; }

        /// <summary>
        /// Allows for logging filters and errors.
        /// </summary>
        public TextWriter Log
        {
            set
            {
                if (value != null)
                {
                    Logger = new SimpleTextLogger(value);
                }
            }
        }
        
        #region Query Methods

        /// <summary>
        /// Creates a query against the directory.
        /// </summary>
        /// <typeparam name="T">Directory type</typeparam>
        /// <param name="scope">Determines the depth at which the search is performed</param>
        /// <param name="namingContext">Optional naming context to override the mapped naming context.</param>
        /// <returns></returns>
        public IQueryable<T> Query<T>(SearchScope scope = SearchScope.Subtree, string namingContext = null) where T : class
        {
            return Query<T>(scope, namingContext, null);
        }

        private IQueryable<T> Query<T>(SearchScope scope, string namingContext, string objectClass = null, IEnumerable<string> objectClasses = null, string objectCategory = null) where T : class
        {
            try
            {
                if (_disposed) throw new ObjectDisposedException(GetType().FullName);
                var mapping = _configuration.Mapper.Map<T>(namingContext, objectClass, objectClasses, objectCategory);
                var provider = new DirectoryQueryProvider(_connection, scope, mapping, _configuration.PagingEnabled)
                                   {
                                       Log = Logger, 
                                       MaxPageSize = _configuration.ServerMaxPageSize, 
                                       NamingContext = namingContext
                                   };
                var directoryQuery = new DirectoryQuery<T>(provider);
                _providers.Add(provider);
                return directoryQuery;
            }
            catch (Exception ex)
            {
                if (Logger != null) Logger.Error(ex);
                throw;
            }
        }

        /// <summary>
        /// Creates a query against the directory.
        /// </summary>
        /// <typeparam name="T">Directory type</typeparam>
        /// <param name="example">An anonymous object that can be used for auto mapping.</param>
        /// <param name="namingContext">The place in the directory from which you want to start your search.</param>
        /// <param name="objectClass">The object class in the directory for the type.</param>
        /// <param name="objectClasses">The object classes in the directory for the type.</param>
        /// <param name="objectCategory">The object category in the directory for the type</param>
        /// <returns></returns>
        public IQueryable<T> Query<T>(T example, string namingContext, string objectClass = null, IEnumerable<string> objectClasses = null, string objectCategory = null) where T : class
        {
            return Query<T>(SearchScope.Subtree, namingContext, objectClass, objectClasses, objectCategory);
        }
        
        /// <summary>
        /// Creates a query against the directory.
        /// </summary>
        /// <typeparam name="T">Directory type</typeparam>
        /// <param name="scope">Determines the depth at which the search is performed</param>
        /// <param name="example">An anonymous object that can be used for auto mapping.</param>
        /// <param name="namingContext">The place in the directory from which you want to start your search.</param>
        /// <param name="objectClass">The object class in the directory for the type.</param>
        /// <param name="objectClasses">The object classes in the directory for the type.</param>
        /// <param name="objectCategory">The object category in the directory for the type</param>
        /// <returns></returns>
        public IQueryable<T> Query<T>(T example, SearchScope scope, string namingContext, string objectClass = null, IEnumerable<string> objectClasses = null, string objectCategory = null) where T : class
        {
            return Query<T>(scope, namingContext, objectClass, objectClasses, objectCategory);
        }

        /// <summary>
        /// Creates a query against the directory for a dynamic type.
        /// </summary>
        /// <param name="namingContext">The place in the directory from which you want to start your search.</param>
        /// <param name="scope">Determines the depth at which the search is performed</param>
        /// <param name="objectClass">The object class in the directory for the type.</param>
        /// <param name="objectClasses">The object classes in the directory for the type.</param>
        /// <param name="objectCategory">The object category in the directory for the type</param>
        /// <returns></returns>
        public IQueryable<IDirectoryAttributes> Query(string namingContext, SearchScope scope = SearchScope.Subtree, string objectClass = null,
            IEnumerable<string> objectClasses = null, string objectCategory = null)
        {
            try
            {
                if (_disposed) throw new ObjectDisposedException(GetType().FullName);
                var mapping = new DynamicObjectMapping(namingContext, objectClasses, objectCategory, objectClass);
                var provider = new DirectoryQueryProvider(_connection, scope, mapping, _configuration.PagingEnabled) { Log = Logger, IsDynamic = true, MaxPageSize = _configuration.ServerMaxPageSize };
                var directoryQuery = new DirectoryQuery<IDirectoryAttributes>(provider);
                _providers.Add(provider);
                return directoryQuery;
            }
            catch (Exception ex)
            {
                if (Logger != null) Logger.Error(ex);

                throw;
            }
        }

        #endregion Query Methods

        /// <summary>
        /// List server information from RootDSE.
        /// </summary>
        /// <param name="attributes">
        /// Specify specific attributes to load.  Some LDAP servers require an explicit request for certain attributes.
        /// </param>
        /// <returns></returns>
        public IEnumerable<KeyValuePair<string, object>> ListServerAttributes(params string[] attributes)
        {
            try
            {
                if (_disposed) throw new ObjectDisposedException(GetType().FullName);
                using (var provider = new DirectoryQueryProvider(
                    _connection, SearchScope.Base, new ServerObjectMapping(), _configuration.PagingEnabled) { Log = Logger, IsDynamic = true, MaxPageSize = _configuration.ServerMaxPageSize })
                {
                    var directoryQuery = new DirectoryQuery<IDirectoryAttributes>(provider);

                    var query = directoryQuery
                        .FilterWith("(objectClass=*)")
                        .Select(attributes);

                    var results = _configuration.PagingEnabled
                        ? query.FirstOrDefault()
                        : query.ToList().FirstOrDefault();

                    return results == null
                                ? new Dictionary<string, object>()
                                : results.AsEnumerable();
                }
            }
            catch (Exception ex)
            {
                if (Logger != null) Logger.Error(ex);
                throw;
            }
        }

        /// <summary>
        /// Retrieves the attributes from the directory using the distinguished name.  <see cref="SearchScope.Base"/> is used.
        /// </summary>
        /// <param name="distinguishedName">The distinguished name to look for.</param>
        /// <param name="attributes">The attributes to load.</param>
        /// <returns></returns>
        public IDirectoryAttributes GetByDN(string distinguishedName, params string[] attributes)
        {
            try
            {
                if (_disposed) throw new ObjectDisposedException(GetType().FullName);
                var request = new SearchRequest { DistinguishedName = distinguishedName, Scope = SearchScope.Base };
                request.Attributes.AddRange(attributes);

                var transformer = new DynamicResultTransformer();

                if (Logger != null && Logger.TraceEnabled) Logger.Trace(request.ToLogString());

                var response = _connection.SendRequest(request) as SearchResponse;

                response.AssertSuccess();

                // ReSharper disable PossibleNullReferenceException
                return (response.Entries.Count == 0
                // ReSharper restore PossibleNullReferenceException
                        ? transformer.Default()
                        : transformer.Transform(response.Entries[0])) as IDirectoryAttributes;
            }
            catch (Exception ex)
            {
                if (ex.Message.Equals("The object does not exist."))
                {
                    if (Logger != null) Logger.Error(ex, string.Format("'{0}' does not exist.", distinguishedName));
                    else throw new DirectoryOperationException(string.Format("'{0}' does not exist.", distinguishedName), ex);
                }
                else
                {
                    if (Logger != null) Logger.Error(ex, string.Format("An error occurred while trying to retrieve '{0}'.  See inner exception for more details", distinguishedName));
                    else throw new DirectoryOperationException(string.Format("An error occurred while trying to retrieve '{0}'.  See inner exception for more details", distinguishedName), ex);
                }
                throw;
            }
        }

        /// <summary>
        /// Retrieves the mapped class from the directory using the distinguished name.  <see cref="SearchScope.Base"/> is used.
        /// </summary>
        /// <param name="distinguishedName">The distinguished name to look for.</param>
        /// <typeparam name="T">The type of mapped object</typeparam>
        /// <returns></returns>
        public T GetByDN<T>(string distinguishedName) where T : class
        {
            try
            {
                if (_disposed) throw new ObjectDisposedException(GetType().FullName);
                var mapping = _configuration.Mapper.Map<T>();

                var request = new SearchRequest { DistinguishedName = distinguishedName, Scope = SearchScope.Base };

                foreach (var property in mapping.Properties.Values)
                {
                    request.Attributes.Add(property);
                }

                var transformer = new ResultTransformer(mapping.Properties, mapping);

                if (Logger != null && Logger.TraceEnabled) Logger.Trace(request.ToLogString());

                var response = _connection.SendRequest(request) as SearchResponse;
                response.AssertSuccess();

                // ReSharper disable PossibleNullReferenceException
                var entry = (response.Entries.Count == 0
                        ? transformer.Default()
                        : transformer.Transform(response.Entries[0])) as T;
                // ReSharper restore PossibleNullReferenceException

                return entry;
            }
            catch (Exception ex)
            {
                if (ex.Message.Equals("The object does not exist."))
                {
                    if (Logger != null) Logger.Error(ex, string.Format("'{0}' does not exist.", distinguishedName));
                    else throw new DirectoryOperationException(string.Format("'{0}' does not exist.", distinguishedName), ex);
                }
                else
                {
                    if (Logger != null) Logger.Error(ex, string.Format("An error occurred while trying to retrieve '{0}'.  See inner exception for more details", distinguishedName));
                    else throw new DirectoryOperationException(string.Format("An error occurred while trying to retrieve '{0}'.  See inner exception for more details", distinguishedName), ex);
                }
                throw;
            }
        }

        /// <summary>
        /// Adds the entry to the directory and returns the newly saved entry from the directory. If the <paramref name="distinguishedName"/> is
        /// null then a mapped distinguished name property is used.
        /// </summary>
        /// <typeparam name="T">The type of entry.</typeparam>
        /// <param name="entry">The object to save</param>
        /// <param name="distinguishedName">The distinguished name for the entry.</param>
        /// <param name="controls">Any <see cref="DirectoryControl"/>s to be sent with the request</param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException">Thrown if entry is null</exception>
        /// <exception cref="ArgumentException">Thrown if distinguished name is null and there is no mapped distinguished name property.</exception>
        /// <exception cref="MappingException">
        /// Thrown if <paramref name="distinguishedName"/> is null and Distinguished Name is not mapped.  
        /// Thrown if object class or object category have not been mapped.
        /// Thrown if <typeparamref name="T"/> has not been mapped.
        /// </exception>
        /// <exception cref="DirectoryOperationException">Thrown if the add was not successful.</exception>
        /// <exception cref="LdapException">Thrown if the add was not successful.</exception>
        public T Add<T>(T entry, string distinguishedName = null, DirectoryControl[] controls = null) where T : class
        {
            try
            {
                if (_disposed) throw new ObjectDisposedException(GetType().FullName);
                if (entry == null) throw new ArgumentNullException("entry");
                var objectMapping = _configuration.Mapper.GetMapping<T>();
                if (objectMapping == null) throw new MappingException("Cannot add an unmapped class.");

                var attributes = new List<DirectoryAttribute>();

                if (objectMapping.ObjectClasses == null || !objectMapping.ObjectClasses.Any())
                {
                    if (objectMapping.ObjectCategory.IsNullOrEmpty()) 
                        throw new MappingException("To add a new entry an Object Class or Object Category must be mapped.");

                    attributes.Add(new DirectoryAttribute("objectClass", objectMapping.ObjectCategory));
                }
                else
                {
                    attributes.Add(new DirectoryAttribute("objectClass", objectMapping.ObjectClasses.ToArray()));
                }

                AddRequest request;
                if (!distinguishedName.IsNullOrEmpty())
                {
                    request = new AddRequest(distinguishedName, attributes.ToArray());
                }
                else
                {
                    var distinguishedNameMapping = objectMapping.GetDistinguishedNameMapping();

                    if (distinguishedNameMapping == null) throw new MappingException("Distinguished Name must be mapped.");

                    distinguishedName = distinguishedNameMapping.GetValue(entry) as string;

                    if (distinguishedName.IsNullOrEmpty()) throw new ArgumentException("Must have a distinguished name mapped property or a distinguished name value must be passed as a parameter.");

                    request = new AddRequest(distinguishedName, attributes.ToArray());
                }

                var directoryAttributes = objectMapping.GetUpdateablePropertyMappings()
                    .Select(pm => pm.GetDirectoryAttribute(entry))
                    .Where(da => da.Count > 0);
                foreach (var da in directoryAttributes)
                {
                    request.Attributes.Add(da);
                }

                if (controls != null)
                {
                    request.Controls.AddRange(controls);
                }
                if (Logger != null && Logger.TraceEnabled) Logger.Trace(request.ToLogString());

                var response = _connection.SendRequest(request) as AddResponse;
                response.AssertSuccess();
            }
            catch (Exception ex)
            {
                if (ex.Message.Equals("The object exists."))
                {
                    if (Logger != null) Logger.Error(ex, string.Format("'{0}' already exists.", distinguishedName));
                    else throw new DirectoryOperationException(string.Format("'{0}' already exists.", distinguishedName), ex);
                }
                else
                {
                    if (Logger != null) Logger.Error(ex, string.Format("An error occurred while trying to add '{0}'.  See inner exception for more details", distinguishedName));
                    else throw new DirectoryOperationException(string.Format("An error occurred while trying to add '{0}'.  See inner exception for more details", distinguishedName), ex);
                }
                throw;
            }

            return GetByDN<T>(distinguishedName);
        }

        /// <summary>
        /// Adds the entry to the directory and returns the newly saved entry from the directory.
        /// </summary>
        /// <param name="distinguishedName">The distinguished name for the entry.</param>
        /// <param name="attributes">The attributes for the entry</param>
        /// <param name="controls">Any <see cref="DirectoryControl"/>s to be sent with the request</param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="attributes"/> or <paramref name="distinguishedName"/> is null.
        /// </exception>
        /// <exception cref="DirectoryOperationException">Thrown if the add was not successful.</exception>
        /// <exception cref="LdapException">Thrown if the operation fails.</exception>
        public IDirectoryAttributes Add(string distinguishedName, IDirectoryAttributes attributes, params DirectoryControl[] controls)
        {
            try
            {
                if (distinguishedName.IsNullOrEmpty()) throw new ArgumentNullException("distinguishedName");
                if (_disposed) throw new ObjectDisposedException(GetType().FullName);

                if (attributes == null)
                    throw new ArgumentNullException("attributes");

                var request = new AddRequest(distinguishedName, attributes.GetChangedAttributes().Where(da => da.Count > 0).ToArray());
                if (controls != null)
                {
                    request.Controls.AddRange(controls);
                }
                if (Logger != null && Logger.TraceEnabled) Logger.Trace(request.ToLogString());

                var response = _connection.SendRequest(request) as AddResponse;
                response.AssertSuccess();
            }
            catch (Exception ex)
            {
                if (ex.Message.Equals("The object exists."))
                {
                    if (Logger != null) Logger.Error(ex, string.Format("'{0}' already exists.", distinguishedName));
                    else throw new DirectoryOperationException(string.Format("'{0}' already exists.", distinguishedName), ex);
                }
                else
                {
                    if (Logger != null) Logger.Error(ex, string.Format("An error occurred while trying to add '{0}'.  See inner exception for more details", distinguishedName));
                    else throw new DirectoryOperationException(string.Format("An error occurred while trying to add '{0}'.  See inner exception for more details", distinguishedName), ex);
                }
                throw;
            }

            return GetByDN(distinguishedName);
        }
        
        /// <summary>
        /// Deletes an entry from the directory.
        /// </summary>
        /// <param name="distinguishedName">The distinguished name of the entry</param><param name="controls">Any <see cref="DirectoryControl"/>s to be sent with the request</param>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="distinguishedName"/> is null, empty or white space.</exception>
        /// <exception cref="DirectoryOperationException">Thrown if the operation fails.</exception>
        /// <exception cref="LdapException">Thrown if the operation fails.</exception>
        public void Delete(string distinguishedName, params DirectoryControl[] controls)
        {
            try
            {
                if (_disposed) throw new ObjectDisposedException(GetType().FullName);
                if (distinguishedName.IsNullOrEmpty())
                    throw new ArgumentNullException("distinguishedName");

                var request = new DeleteRequest(distinguishedName);
                if (controls != null)
                {
                    request.Controls.AddRange(controls);
                }
                if (Logger != null && Logger.TraceEnabled) Logger.Trace(request.ToLogString());

                var response = _connection.SendRequest(request) as DeleteResponse;
                response.AssertSuccess();
            }
            catch (Exception ex)
            {
                if (ex.Message.Equals("The object does not exist."))
                {
                    if (Logger != null) Logger.Error(ex, string.Format("'{0}' does not exist.", distinguishedName));
                    else throw new DirectoryOperationException(string.Format("'{0}' does not exist.", distinguishedName), ex);
                }
                else
                {
                    if (Logger != null) Logger.Error(ex, string.Format("An error occurred while trying to delete '{0}'.  See inner exception for more details", distinguishedName));
                    else throw new DirectoryOperationException(string.Format("An error occurred while trying to delete '{0}'.  See inner exception for more details", distinguishedName), ex);
                }
                throw;
            }
        }

        /// <summary>
        /// Updates the entry in the directory and returns the updated version from the directory. If the <paramref name="distinguishedName"/> is
        /// null then a mapped distinguished name property is used.
        /// </summary>
        /// <param name="entry">The entry to update</param>
        /// <param name="distinguishedName">The distinguished name for the entry.</param>
        /// <param name="controls">Any <see cref="DirectoryControl"/>s to be sent with the request</param>
        /// <typeparam name="T">The type of entry.</typeparam>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException">Thrown if entry is null</exception>
        /// <exception cref="MappingException">
        /// Thrown if <paramref name="distinguishedName"/> is null and Distinguished Name is not mapped.  
        /// Thrown if object class or object category have not been mapped.
        /// Thrown if <typeparamref name="T"/> has not been mapped.
        /// </exception>
        /// <exception cref="ArgumentException">Thrown if distinguished name is null and there is no mapped distinguished name property.</exception>
        /// <exception cref="InvalidOperationException">Thrown if <paramref name="entry"/> is <see cref="DirectoryObjectBase"/> but the entry is not tracking changes.</exception>
        /// <exception cref="DirectoryOperationException">Thrown if the operation is not successful</exception>
        /// <exception cref="LdapException">Thrown if the operation is not successful</exception>
        public T Update<T>(T entry, string distinguishedName = null, DirectoryControl[] controls = null) where T : class
        {
            try
            {
                if (_disposed) throw new ObjectDisposedException(GetType().FullName);
                if (entry == null) throw new ArgumentNullException("entry");

                var objectMaping = _configuration.Mapper.GetMapping<T>();
                if (objectMaping == null) throw new MappingException("Cannot add an unmapped class.");

                if (distinguishedName.IsNullOrEmpty())
                {
                    var distinguishedNameMapping = objectMaping.GetDistinguishedNameMapping();

                    if (distinguishedNameMapping == null) throw new MappingException("Distinguished name must be mapped.");

                    distinguishedName = distinguishedNameMapping.GetValue(entry) as string;

                    if (distinguishedName.IsNullOrEmpty()) throw new ArgumentException("The distinguished name cannot be null or empty.");
                }

                var modifications = new List<DirectoryAttributeModification>();
                var directoryObject = entry as IDirectoryObject;

                if (directoryObject == null)
                {
                    modifications.AddRange(objectMaping.GetUpdateablePropertyMappings().Select(mapping => mapping.GetDirectoryAttributeModification(entry)));
                }
                else
                {
                    var changes = directoryObject.GetChanges(objectMaping);
                    modifications.AddRange(changes);
                }

                if (modifications.Count == 0) return entry;

                var request = new ModifyRequest(distinguishedName, modifications.ToArray());
                if (controls != null)
                {
                    request.Controls.AddRange(controls);
                }
                if (Logger != null && Logger.TraceEnabled) Logger.Trace(request.ToLogString());

                var response = _connection.SendRequest(request) as ModifyResponse;
                response.AssertSuccess();
            }
            catch (Exception ex)
            {
                if (ex.Message.Equals("The object does not exist."))
                {
                    if (Logger != null) Logger.Error(ex, string.Format("'{0}' does not exist.", distinguishedName));
                    else throw new DirectoryOperationException(string.Format("'{0}' does not exist.", distinguishedName), ex);
                }
                else
                {
                    if (Logger != null) Logger.Error(ex, string.Format("An error occurred while trying to update '{0}'.  See inner exception for more details", distinguishedName));
                    else throw new DirectoryOperationException(string.Format("An error occurred while trying to update '{0}'.  See inner exception for more details", distinguishedName), ex);
                }
                throw;
            }

            return GetByDN<T>(distinguishedName);
        }

        /// <summary>
        /// Updates the entry in the directory and returns the updated version from the directory.
        /// </summary>
        /// <param name="attributes">The attributes for the entry.</param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="attributes"/> is null.
        /// </exception>
        /// <exception cref="DirectoryOperationException">Thrown if the operation fails</exception>
        /// <exception cref="LdapException">Thrown if the operation fails</exception>
        public IDirectoryAttributes Update(IDirectoryAttributes attributes)
        {
            return Update(attributes, default(DirectoryControl[]));
        }

        /// <summary>
        /// Updates the entry in the directory and returns the updated version from the directory.
        /// </summary>
        /// <param name="attributes">The attributes for the entry.</param>
        /// <param name="controls">Any <see cref="DirectoryControl"/>s to be sent with the request</param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="attributes"/> is null.
        /// </exception>
        /// <exception cref="DirectoryOperationException">Thrown if the operation fails</exception>
        /// <exception cref="LdapException">Thrown if the operation fails</exception>
        public IDirectoryAttributes Update(IDirectoryAttributes attributes, DirectoryControl[] controls)
        {
            string distinguishedName = null;
            try
            {
                if (_disposed) throw new ObjectDisposedException(GetType().FullName);
                if (attributes == null) throw new ArgumentNullException("attributes");
                distinguishedName = attributes.DistinguishedName;
                
                var changes = attributes.GetChangedAttributes();

                if (changes.Any())
                {
                    var request = new ModifyRequest(distinguishedName, changes.ToArray());
                    if (controls != null)
                    {
                        request.Controls.AddRange(controls);
                    }
                    if (Logger != null && Logger.TraceEnabled) Logger.Trace(request.ToLogString());

                    var response = _connection.SendRequest(request) as ModifyResponse;
                    response.AssertSuccess();
                }
            }
            catch (Exception ex)
            {
                if (ex.Message.Equals("The object does not exist."))
                {
                    if (Logger != null) Logger.Error(ex, string.Format("'{0}' does not exist.", distinguishedName));
                    else throw new DirectoryOperationException(string.Format("'{0}' does not exist.", distinguishedName), ex);
                }
                else
                {
                    if (Logger != null) Logger.Error(ex, string.Format("An error occurred while trying to update '{0}'.  See inner exception for more details", distinguishedName));
                    else throw new DirectoryOperationException(string.Format("An error occurred while trying to update '{0}'.  See inner exception for more details", distinguishedName), ex);
                }

                throw;
            }

            return GetByDN(distinguishedName);
        }

        /// <summary>
        /// Adds the attribte to an entry.
        /// </summary>
        /// <param name="distinguishedName">The entry</param>
        /// <param name="attributeName">The name of the attribute</param>
        /// <param name="value">The value for the entry.</param>
        /// <param name="controls">Any <see cref="DirectoryControl"/>s to be sent with the request</param>
        /// <exception cref="DirectoryOperationException">Thrown if the operation fails.</exception>
        /// <exception cref="LdapConnection">Thrown if the operation fails.</exception>
        public void AddAttribute(string distinguishedName, string attributeName, object value = null, DirectoryControl[] controls = null)
        {
            try
            {
                if (_disposed) throw new ObjectDisposedException(GetType().FullName);
                if (distinguishedName.IsNullOrEmpty())
                    throw new ArgumentNullException("distinguishedName");

                var modification = value.ToDirectoryModification(attributeName, DirectoryAttributeOperation.Add);

                var request = new ModifyRequest(distinguishedName, modification);
                if (controls != null)
                {
                    request.Controls.AddRange(controls);
                }
                if (Logger != null && Logger.TraceEnabled) Logger.Trace(request.ToLogString());

                ModifyResponse response;
                try
                {
                    response = _connection.SendRequest(request) as ModifyResponse;
                }
                catch (DirectoryOperationException ex)
                {
                    if (ex.Message.Equals("The object does not exist."))
                    {
                        throw new DirectoryOperationException(string.Format("'{0}' does not exist.", distinguishedName), ex);
                    }
                    throw new DirectoryOperationException(
                        string.Format(
                            "An error occurred while trying to add attribute '{0}' for '{1}'.  See inner exception for more details",
                            attributeName, distinguishedName), ex);
                }
                response.AssertSuccess();
            }
            catch (Exception ex)
            {
                if (ex.Message.Equals("The object does not exist."))
                {
                    if (Logger != null) Logger.Error(ex, string.Format("'{0}' does not exist.", distinguishedName));
                    else throw new DirectoryOperationException(string.Format("'{0}' already exists.", distinguishedName), ex);
                }
                else
                {
                    if (Logger != null) Logger.Error(ex, string.Format("An error occurred while trying to add attribute '{0}' for '{1}'.  See inner exception for more details", attributeName, distinguishedName));
                    else throw new DirectoryOperationException(string.Format("An error occurred while trying to add attribute '{0}' for '{1}'.  See inner exception for more details", attributeName, distinguishedName), ex);
                }

                throw;
            }
        }

        /// <summary>
        /// Removes the attribte from an entry.
        /// </summary>
        /// <param name="distinguishedName">The entry</param>
        /// <param name="attributeName">The name of the attribute</param>
        /// <param name="controls">Any <see cref="DirectoryControl"/>s to be sent with the request</param>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="distinguishedName"/> is null, empty or white space.</exception>
        /// <exception cref="DirectoryOperationException">Thrown if the operation fails.</exception>
        /// <exception cref="LdapConnection">Thrown if the operation fails.</exception>
        public void DeleteAttribute(string distinguishedName, string attributeName, params DirectoryControl[] controls)
        {
            try
            {
                if (_disposed) throw new ObjectDisposedException(GetType().FullName);
                if (distinguishedName.IsNullOrEmpty())
                    throw new ArgumentNullException("distinguishedName");

                var modification = new DirectoryAttributeModification
                {
                    Operation = DirectoryAttributeOperation.Delete,
                    Name = attributeName
                };

                var request = new ModifyRequest(distinguishedName, modification);
                if (controls != null)
                {
                    request.Controls.AddRange(controls);
                }
                if (Logger != null && Logger.TraceEnabled) Logger.Trace(request.ToLogString());

                var response = _connection.SendRequest(request) as ModifyResponse;
                response.AssertSuccess();
            }
            catch (Exception ex)
            {
                if (ex.Message.Equals("The object does not exist."))
                {
                    if (Logger != null) Logger.Error(ex, string.Format("'{0}' does not exist.", distinguishedName));
                    else throw new DirectoryOperationException(string.Format("'{0}' does not exist.", distinguishedName), ex);
                }
                else
                {
                    if (Logger != null) Logger.Error(ex, string.Format("An error occurred while trying to delete attribute '{0}' for '{1}'.  See inner exception for more details", attributeName, distinguishedName));
                    else throw new DirectoryOperationException(string.Format("An error occurred while trying to delete attribute '{0}' for '{1}'.  See inner exception for more details", attributeName, distinguishedName), ex);
                }

                throw;
            }
        }

        /// <summary>
        /// Moves the entry from one container to another without modifying the entry's name.
        /// </summary>
        /// <param name="currentDistinguishedName">The entry's current distinguished name</param>
        /// <param name="newNamingContext">The new container for the entry</param>
        /// <param name="controls">Any <see cref="DirectoryControl"/>s to be sent with the request</param>
        /// <exception cref="ArgumentException">
        /// Thrown if <paramref name="currentDistinguishedName"/> has an invalid format.
        /// </exception>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="currentDistinguishedName"/>
        /// or <paramref name="newNamingContext"/> are null, empty or white space.
        /// </exception>
        /// <exception cref="DirectoryOperationException">Thrown if the operation fails.</exception>
        /// <exception cref="LdapConnection">Thrown if the operation fails.</exception>
        public string MoveEntry(string currentDistinguishedName, string newNamingContext, params DirectoryControl[] controls)
        {
            try
            {
                if (_disposed) throw new ObjectDisposedException(GetType().FullName);
                if (currentDistinguishedName.IsNullOrEmpty())
                    throw new ArgumentNullException("currentDistinguishedName");

                if (newNamingContext.IsNullOrEmpty())
                    throw new ArgumentNullException("newNamingContext");

                var name = DnParser.GetEntryName(currentDistinguishedName);

                var request = new ModifyDNRequest
                {
                    DistinguishedName = currentDistinguishedName,
                    NewParentDistinguishedName = newNamingContext,
                    NewName = name
                };
                if (controls != null)
                {
                    request.Controls.AddRange(controls);
                }
                if (Logger != null && Logger.TraceEnabled) Logger.Trace(request.ToLogString());

                var response = _connection.SendRequest(request) as ModifyDNResponse;
                response.AssertSuccess();

                return name + "," + newNamingContext;
            }
            catch (Exception ex)
            {
                if (Logger != null) Logger.Error(ex, string.Format("An error occurred while trying to move entry '{0}' to '{1}'.  See inner exception for more details", currentDistinguishedName, newNamingContext));
                else throw new DirectoryOperationException(string.Format("An error occurred while trying to move entry '{0}' to '{1}'.  See inner exception for more details",currentDistinguishedName, newNamingContext), ex);

                throw;
            }
        }

        /// <summary>
        /// Renames the entry within the same container. The <paramref name="newName"/> can be in the format
        /// XX=New Name or just New Name.
        /// </summary>
        /// <param name="currentDistinguishedName">The entry's current distinguished name</param>
        /// <param name="newName">The new name of the entry</param>
        /// <param name="controls">Any <see cref="DirectoryControl"/>s to be sent with the request</param>
        /// <exception cref="ArgumentException">
        /// Thrown if <paramref name="currentDistinguishedName"/> has an invalid format.
        /// </exception>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="currentDistinguishedName"/>
        /// or <paramref name="newName"/> are null, empty or white space.
        /// </exception>
        /// <exception cref="DirectoryOperationException">Thrown if the operation fails.</exception>
        /// <exception cref="LdapConnection">Thrown if the operation fails.</exception>
        public string RenameEntry(string currentDistinguishedName, string newName, params DirectoryControl[] controls)
        {
            try
            {
                if (_disposed) throw new ObjectDisposedException(GetType().FullName);
                if (currentDistinguishedName.IsNullOrEmpty())
                    throw new ArgumentNullException("currentDistinguishedName");

                if (newName.IsNullOrEmpty())
                    throw new ArgumentNullException("newName");

                newName = DnParser.FormatName(newName, currentDistinguishedName);
                var container = DnParser.GetEntryContainer(currentDistinguishedName);

                var request = new ModifyDNRequest
                {
                    DistinguishedName = currentDistinguishedName,
                    NewParentDistinguishedName = container,
                    NewName = newName
                };
                if (controls != null)
                {
                    request.Controls.AddRange(controls);
                }
                if (Logger != null && Logger.TraceEnabled) Logger.Trace(request.ToLogString());

                var response = _connection.SendRequest(request) as ModifyDNResponse;
                response.AssertSuccess();

                return newName + "," + container;
            }
            catch (Exception ex)
            {
                if (Logger != null) Logger.Error(ex, string.Format("An error occurred while trying to rename entry '{0}' to '{1}'.  See inner exception for more details", currentDistinguishedName, newName));
                else throw new DirectoryOperationException(string.Format("An error occurred while trying to rename entry '{0}' to '{1}'.  See inner exception for more details",currentDistinguishedName, newName), ex);
                
                throw;
            }
        }

        /// <summary>
        /// Finalizer that disposes of this class.
        /// </summary>
        ~DirectoryContext()
        {
            if (!_disposed) InternalDispose();
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            if (_disposed) throw new ObjectDisposedException(GetType().FullName);

            _disposed = true;
            InternalDispose();
            GC.SuppressFinalize(this);
        }

        private void InternalDispose()
        {
            var disposeOfConnection = _disposeOfConnection;
            var providers = _providers;

            if (providers != null)
            {
                foreach (var provider in providers)
                {
                    provider.Dispose();
                }
                providers.Clear();
                providers = null;
            }

            if (disposeOfConnection)
            {
                var connection = _connection;
                if (connection != null)
                {
                    var configuration = _configuration;
                   
                    if (configuration != null)
                    {
                        var connectionFactory = configuration.ConnectionFactory;
                        if (connectionFactory != null)
                        {
                            connectionFactory.ReleaseConnection(connection);
                            connectionFactory = null;
                        }
                        else
                        {
                            connection.Dispose();
                        }
                        configuration = null;
                    }
                    else
                    {
                        connection.Dispose();
                    }
                    connection = null;
                }
            }

            _configuration = null;
            _connection = null;
            Logger = null;
        }
    }
}
